Skip to content

13 状态机魔鬼 - TCP 11 种状态变迁及模拟重现

讲完前面建立连接、断开连接的过程,整个 TCP 协议的 11 种状态都出现了。TCP 之所以复杂,是因为它是一个有状态的协议。如果这个时候祭出下面的 TCP 状态变化图,估计大多数人都会懵圈,不要慌,我们会把上面的状态一一解释清楚。

img

上面这个图是网络上有人用 Latex 画出来了,很赞。不过有一处小错误,我修改了一下,如果感兴趣的话可以从我的 github 上进行下载,链接:tcp-state-machine.tex,在 overleaf 的网站可以进行实时预览。

01 CLOSED

这个状态是一个「假想」的状态,是 TCP 连接还未开始建立连接或者连接已经彻底释放的状态。因此 CLOSED 状态也无法通过 netstat 或者 lsof 等工具看到。

从图中可以看到,从 CLOSE 状态转换为其它状态有两种可能:主动打开(Active Open)和被动打开(Passive Open)

  • 被动打开:一般来说,服务端会监听一个特定的端口,等待客户端的新连接,同时会进入 LISTEN 状态,这种被称为「被动打开」
  • 主动打开:客户端主动发送一个 SYN 包准备三次握手,被称为「主动打开(Active Open)」

02 LISTEN

一端(通常是服务端)调用 bind、listen 系统调用监听特定端口时进入到 LISTEN 状态,等待客户端发送 SYN 报文三次握手建立连接。

在 Java 中只用一行代码就可以构造一个 listen 状态的 socket。

c
ServerSocket serverSocket = new ServerSocket(9999);

ServerSocket 的构造器函数最终调用了 bind、listen,接下来就可以调用 accept 接收客户端连接请求了。

使用 netstat 进行查看

bash
netstat -tnpa | grep -i 9999
tcp6       0      0 :::9999     :::*                    LISTEN      20096/java

处于 LISTEN 状态的连接收到 SYN 包以后会发送 SYN+ACK 给对端,同时进入 SYN-RCVD 阶段

03 SYN-SENT

客户端发送 SYN 报文等待 ACK 的过程进入 SYN-SENT 状态。同时会开启一个定时器,如果超时还没有收到 ACK 会重发 SYN。

使用 packetdrill 可以非常快速的构造一个处于 SYN-SENT 状态的连接,完整的代码见:syn_sent.pkt

c
+0 socket(..., SOCK_STREAM, IPPROTO_TCP) = 3

+0 connect(3, ..., ...) = -1

运行上面的脚本,然后使用 netstat 命令查看连接状态 l

bash
netstat -atnp | grep -i 8080
tcp        0      1 192.168.46.26:42678     192.0.2.1:8080          SYN_SENT    3897/packetdrill

04 SYN-RCVD

服务端收到 SYN 报文以后会回复 SYN+ACK,然后等待对端 ACK 的时候进入 SYN-RCVD,完整的代码见:state_syn_rcvd.pkt

c
+0  < S 0:0(0) win 65535  <mss 100>
+0  > S. 0:0(0) ack 1 <...>
// 故意注释掉下面这一行
// +.1 < . 1:1(0) ack 1 win 65535

05 ESTABLISHED

SYN-SENT 或者 SYN-RCVD 状态的连接收到对端确认 ACK 以后进入 ESTABLISHED 状态,连接建立成功。

把上面例子中脚本的注释取消掉,三次握手成功就会进入 ESTABLISHED 状态。

从图中可以看到 ESTABLISHED 状态的连接有两种可能的状态转换方式:

  • 调用 close 等系统调用主动关闭连接,这个时候会发送 FIN 包给对端,同时自己进入 FIN-WAIT-1 状态
  • 收到对端的 FIN 包,执行被动关闭,收到 FIN 包以后会回复 ACK,同时自己进入 CLOSE-WAIT 状态

06 FIN-WAIT-1

主动关闭的一方发送了 FIN 包,等待对端回复 ACK 时进入 FIN-WAIT-1 状态。

模拟的 packetdrill 脚本见:state_fin_wait_1.pkt

c
+0  < S 0:0(0) win 65535  <mss 100>
+0  > S. 0:0(0) ack 1 <...>
.1 < . 1:1(0) ack 1 win 65535

+.1 accept(3, ..., ...) = 4

+.1 close(4) = 0

执行上的脚本,使用 netstat 就可以看到 FIN_WAIT1 状态的连接了

bash
netstat -tnpa | grep 8080
tcp        0      0 192.168.73.207:8080     0.0.0.0:*               LISTEN      -
tcp        0      1 192.168.73.207:8080     192.0.2.1:52859         FIN_WAIT1   -

FIN_WAIT1 状态的切换如下几种情况

  • 当收到 ACK 以后,FIN-WAIT-1 状态会转换到 FIN-WAIT-2 状态
  • 当收到 FIN 以后,会回复对端 ACKFIN-WAIT-1 状态会转换到 CLOSING 状态
  • 当收到 FIN+ACK 以后,会回复对端 ACKFIN-WAIT-1 状态会转换到 TIME_WAIT 状态,跳过了 FIN-WAIT-2 状态

07 FIN-WAIT-2

处于 FIN-WAIT-1 状态的连接收到 ACK 确认包以后进入 FIN-WAIT-2 状态,这个时候主动关闭方的 FIN 包已经被对方确认,等待被动关闭方发送 FIN 包。

模拟的脚本见:state_fin_wait_2.pkt,核心代码如下

c
+0  < S 0:0(0) win 65535  <mss 100>
+0  > S. 0:0(0) ack 1 <...>
.1 < . 1:1(0) ack 1 win 65535
+.1  accept(3, ..., ...) = 4

+.1 close(4) = 0

+.1 < . 1:1(0) ack 2 win 257

执行上的脚本,使用 netstat 就可以看到 FIN_WAIT2 状态的连接了

bash
netstat -tnpa | grep 8080
tcp        0      0 192.168.81.69:8080      0.0.0.0:*               LISTEN      -
tcp        0      0 192.168.81.69:8080      192.0.2.1:34131         FIN_WAIT2   -

当收到对端的 FIN 包以后,主动关闭方进入 TIME_WAIT 状态

08 CLOSE-WAIT

当有一方想关闭连接的时候,调用 close 等系统调用关闭 TCP 连接会发送 FIN 包给对端,这个被动关闭方,收到 FIN 包以后进入 CLOSE-WAIT 状态。

完整的代码见:state_close_wait.pkt

c
+.1 < F. 1:1(0) win 65535  <mss 100>

+0 > . 1:1(0) ack 2 <...>

执行上的脚本,使用 netstat 就可以看到 CLOSE_WAIT 状态的连接了

bash
sudo netstat -tnpa | grep -i 8080
tcp        0      0 192.168.168.15:8080     0.0.0.0:*               LISTEN      15818/packetdrill
tcp        1      0 192.168.168.15:8080     192.0.2.1:44948         CLOSE_WAIT  15818/packetdrill

当被动关闭方有数据要发送给对端的时候,可以继续发送数据。当没有数据发送给对方时,也会调用 close 等系统调用关闭 TCP 连接,发送 FIN 包给主动关闭的一方,同时进入 LAST-ACK 状态

09 TIME-WAIT

TIME-WAIT 可能是所有状态中面试问的最频繁的一种状态了。这个状态是收到了被动关闭方的 FIN 包,发送确认 ACK 给对端,开启 2MSL 定时器,定时器到期时进入 CLOSED 状态,连接释放。TIME-WAIT 会有专门的文章介绍。

完整的代码见:state_time_wait.pkt

c
// 服务端主动断开连接
+.1 close(4) = 0
+0 > F. 1:1(0) ack 1 <...>

// 向协议栈注入 ACK 包,模拟客户端发送了 ACK
+.1 < . 1:1(0) ack 2 win 257

// 向协议栈注入 FIN,模拟服务端收到了 FIN
+.1 < F. 1:1(0) win 65535  <mss 100>

+0 `sleep 1000000`

执行上的脚本,使用 netstat 就可以看到 TIME-WAIT 状态的连接了

bash
netstat -tnpa | grep -i 8080

tcp        0      0 192.168.210.245:8080    0.0.0.0:*               LISTEN      6297/packetdrill
tcp        0      0 192.168.210.245:8080    192.0.2.1:40091         TIME_WAIT   -

10 LAST-ACK

LAST-ACK 顾名思义等待最后的 ACK。是被动关闭的一方,发送 FIN 包给对端等待 ACK 确认时的状态。

完整的模拟代码见:state_last_ack.pkt

c
// 向协议栈注入 FIN 包,模拟客户端发送了 FIN,主动关闭连接
+.1 < F. 1:1(0) win 65535  <mss 100>
// 预期协议栈会发出 ACK
+0 > . 1:1(0) ack 2 <...>

+.1 close(4) = 0
// 预期服务端会发出 FIN
+0 > F. 1:1(0) ack 2 <...>
sudo netstat -lnpa  | grep 8080                                                                                                                                                                             1
tcp        0      0 192.168.190.26:8080     0.0.0.0:*               LISTEN      6163/packetdrill
tcp        1      1 192.168.190.26:8080     192.0.2.1:36054         LAST_ACK

当收到 ACK 以后,进入 CLOSED 状态,连接释放。

11 CLOSING

CLOSING 状态在「同时关闭」的情况下出现。这里的同时关闭中的「同时」其实并不是时间意义上的同时,而是指的是在发送 FIN 包还未收到确认之前,收到了对端的 FIN 的情况。

我们用一个简单的脚本来模拟 CLOSING 状态。完整的代码见 state-closing.pkt

c
+0.100 write(4, ..., 1000) = 1000

+0 > P. 1:1001(1000) ack 1 <...>

+0.01 < . 1:1(0) ack 1001 win 257

+.1 close(4) = 0

+0 > F. 1001:1001(0) ack 1 <...>

+.1 < F. 1:1(0) ack 1001 win 257

+0 > . 1002:1002(0) ack 2 <...>

运行 packetdrill 执行上面的脚本,同时开启抓包。

使用 netstat 查看当前的连接状态就可以看到 CLOSING 状态了。

bash
netstat -lnpa | grep -i 8080

tcp        0      0 192.168.60.204:8080     0.0.0.0:*               LISTEN      -
tcp        1      1 192.168.60.204:8080     192.0.2.1:55456         CLOSING     -

使用 wireshark 查看如下图所示,完整的抓包文件可以从 github 下载:state-closing.pcap

img

整个过程如下图所示

img

12 小结

到这里,TCP 的 11 种状态就介绍完了,我为了你准备了几道试题,看下自己的掌握的情况吧。

13 作业题

  1. 下列 TCP 连接建立过程描述正确的是:

    • A. 服务端收到客户端的 SYN 包后等待 2*MSL 时间后就会进入 SYN_SENT 状态
    • B. 服务端收到客户端的 ACK 包后会进入 SYN_RCVD 状态
    • C. 当客户端处于 ESTABLISHED 状态时,服务端可能仍然处于 SYN_RCVD 状态
    • D. 服务端未收到客户端确认包,等待 2*MSL 时间后会直接关闭连接
  2. TCP 连接关闭,可能有经历哪几种状态:

    • A. LISTEN
    • B. TIME-WAIT
    • C. LAST-ACK
    • D. SYN-RECEIVED